type VNodeProps = { [key: string]: any; }; type VNode = { tag: string; props: VNodeProps; children: any[]; }; const createElement = (tag: string, props: VNodeProps | null, ...children: any[]): VNode => { return { tag, props: props || {}, children }; }; const createDomElement = (vNode: VNode): HTMLElement => { const element = document.createElement(vNode.tag); Object.entries(vNode.props).forEach(([key, value]) => { if (key.startsWith("on")) { element[key.toLowerCase()] = value; } else { element.setAttribute(key, value); } }); vNode.children.forEach(child => { element.appendChild( typeof child === 'object' ? createDomElement(child) : document.createTextNode(child) ); }); return element; }; let currentComponent: Function | null = null; let currentHookIndex = 0; const hooks = new Map(); const component = (renderFn: Function): VNode => { currentComponent = renderFn; currentHookIndex = 0; const element = renderFn(); currentComponent = null; return element; }; const RNX = (tag: string, props: VNodeProps | null, ...children: any[]): VNode => createElement(tag, props, ...children); const state = (initialValue: T) => { if (!currentComponent) { throw new Error("state() can only be called inside a component function"); } const componentId = currentComponent; if (!hooks.has(componentId)) { hooks.set(componentId, []); } const hookArray = hooks.get(componentId)!; if (hookArray.length <= currentHookIndex) { hookArray.push(initialValue); } const stateIndex = currentHookIndex; const getState = (): T => hookArray[stateIndex]; const setState = (newValue: T) => { hookArray[stateIndex] = newValue; requestAnimationFrame(App.update); // Ensures update is called only once per frame }; currentHookIndex++; return { get: getState, set: setState }; }; const Slot = () => App.currentPage?.(); type Patch = { type: 'CREATE' | 'REMOVE' | 'REPLACE' | 'TEXT' | 'UPDATE'; newVNode?: VNode; propPatches?: { key: string, value: any }[]; childrenPatches?: (Patch | null)[]; }; const diff = (newVNode: VNode, oldVNode: VNode): Patch | null => { if (!oldVNode) { return { type: 'CREATE', newVNode }; } if (!newVNode) { return { type: 'REMOVE' }; } if (newVNode.tag !== oldVNode.tag) { return { type: 'REPLACE', newVNode }; } if (typeof newVNode !== 'object') { if (newVNode !== oldVNode) { return { type: 'TEXT', newVNode }; } else { return null; } } const propPatches: { key: string, value: any }[] = []; const allProps = { ...newVNode.props, ...oldVNode.props }; Object.keys(allProps).forEach(key => { const newVal: any = newVNode.props[key]; const oldVal = oldVNode.props[key]; if (newVal !== oldVal) { propPatches.push({ key, value: newVal }); } }); const childrenPatches: (Patch | null)[] = []; const maxLength = Math.max(newVNode.children.length, oldVNode.children.length); for (let i = 0; i < maxLength; i++) { childrenPatches.push(diff(newVNode.children[i], oldVNode.children[i])); } return { type: 'UPDATE', propPatches, childrenPatches }; }; const patch = (parent: HTMLElement, patches: Patch | null, index = 0) => { if (!patches) return; const element = parent.childNodes[index] as HTMLElement; switch (patches.type) { case 'CREATE': { const newElement = createDomElement(patches.newVNode!); parent.appendChild(newElement); break; } case 'REMOVE': { parent.removeChild(element); break; } case 'REPLACE': { const newElement = createDomElement(patches.newVNode!); parent.replaceChild(newElement, element); break; } case 'TEXT': { element.textContent = patches.newVNode as string; break; } case 'UPDATE': { patches.propPatches!.forEach(({ key, value }) => { if (key.startsWith("on")) { element[key.toLowerCase()] = value; } else { element.setAttribute(key, value); } }); patches.childrenPatches!.forEach((childPatch, i) => { patch(element, childPatch, i); }); break; } } }; const App = { routes: {} as { [path: string]: Function }, layoutComponent: null as Function | null, currentPage: null as (() => VNode) | null, mountPoint: null as HTMLElement | null, vdom: null as VNode | null, setRoutes(routeMap: { [path: string]: Function }) { this.routes = routeMap; }, setLayout(layoutComponent: Function) { this.layoutComponent = layoutComponent; }, mount(selector: string) { this.mountPoint = document.querySelector(selector) as HTMLElement; this.update(); window.onpopstate = () => { this.update(); }; }, update: () => { App.redirect(); const path = (window.location.hash || "#/").slice(1); const routeComponent = App.routes[path] || component(() => html.div({}, html.h1({}, "Page not found"))); App.currentPage = () => routeComponent(); const newVdom = App.layoutComponent!(); const patches = diff(newVdom, App.vdom!); patch(App.mountPoint!, patches); App.vdom = newVdom; App.handleLinks(); }, handleLinks() { const links = document.querySelectorAll("a"); links.forEach(link => { if (link.getAttribute("href")?.startsWith("#")) return; link.setAttribute("href", "#" + link.getAttribute("href")); }); }, redirect() { if (!(window.location.hash.slice(1) === "")) return; window.location.hash = "#/"; } }; // Define functions for HTML elements const tags = [ "div", "header", "main", "footer", "nav", "aside", "section", "a", "p", "button", "h1", "h2", "h3", "h4", "h5", "h6", "span", "script", "style", "hr", "br", "u", "i", "em", "b", "strong", "ol", "li", "ul", "img", "details", "sup", "sub", "input", "form", "label", "base", "address", "pre", "code", "blockquote", "menu", "dl", "dt", "dd", "figure", "figcaption", "small", "cite", "q", "dfn", "abbr", "data", "time", "var", "dir", "samp", "kbd", "mark", "ruby", "bdi", "bdo", "ins", "del", "wbr", "s", "picture", "iframe", "source", "video", "audio", "track", "svg", "math", "area", "table", "caption", "col", "colgroup", "td", "tr", "fieldset", "legend", "datalist", "select", "option", "optgroup", "textarea", "progress", "output", "meter", "noscript", "canvas", "template", "summary", "dialog", "article", "path", "circle", "g", "ellipse" ]; const html: { [key: string]: (props: VNodeProps, ...children: any[]) => VNode } = {}; tags.forEach(tag => { html[tag] = (props: VNodeProps, ...children: any[]) => RNX(tag, props, ...children); }); export { createElement, component, RNX, state, Slot, App, html }; export function Each(array: any[], renderFn): VNode[] { return array.map(renderFn); }